Skip to content

Conversation

@yorhodes
Copy link
Member

@yorhodes yorhodes commented Nov 5, 2025

Description

  • Pipe delay reason from CCTP API through to relayer

Drive-by changes

Related issues

Fixes opaque error

CCIP-read metadata fetch failed for http://localhost:3000/cctp/getCctpAttestation: TypeError: Cannot read properties of undefined (reading 'startsWith')

Backward compatibility

Testing

Manual

$ yarn hyperlane status --private-key $(hypkey) --origin base --relay --dispatchTx 0xf78db32e5c77317a25b94608016ea19e4f22aaf337b1ab39ae6052d0d967c9a1
Hyperlane CLI
Checking status of message 0x6a1e2bbf3e46e356157f7568500429e2f789300fa309add0afc746fe7216fec9 on hyperevm
Message 0x6a1e2bbf3e46e356157f7568500429e2f789300fa309add0afc746fe7216fec9 was not yet delivered
Preparing to relay message 0x6a1e2bbf3e46e356157f7568500429e2f789300fa309add0afc746fe7216fec9
Server http://localhost:3000/cctp/getCctpAttestation responded with error: CCTP attestation is pending due to insufficient_fee
Failed to relay message 0x6a1e2bbf3e46e356157f7568500429e2f789300fa309add0afc746fe7216fec9, Error: Could not fetch CCIP-read metadata

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced attestation handling with improved detection and reporting of delayed states
    • Increased resilience for metadata fetching operations with better error recovery on failed requests
  • Refactor

    • Optimized response handling flow for metadata fetching

@changeset-bot
Copy link

changeset-bot bot commented Nov 5, 2025

⚠️ No Changeset found

Latest commit: 92872d1

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@yorhodes yorhodes changed the title cctp v2 ccip server fix: improve cctp v2 offchain lookup service Nov 5, 2025
@yorhodes yorhodes marked this pull request as ready for review November 5, 2025 22:25
Base automatically changed from deploy-cctp-v2 to main November 6, 2025 22:24
@yorhodes yorhodes enabled auto-merge November 6, 2025 22:25
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 6, 2025

📝 Walkthrough

Walkthrough

This pull request adds CCTP v2 support with PENDING attestation detection and error handling in the attestation service, while refactoring CCIP-read metadata fetching to use persistent response tracking and improved multi-URL retry logic with explicit error handling.

Changes

Cohort / File(s) Summary
CCTP v2 Attestation Handling
typescript/ccip-server/src/services/CCTPAttestationService.ts
Introduces CCTP v2 types (DelayReason, Status) and extends CCTPMessageEntry with eventNonce and optional fields. Adds PENDING attestation detection in getAttestation; logs errors and conditionally logs Prometheus metrics for known delay reasons before throwing descriptive error.
CCIP-read Response Flow Refactoring
typescript/sdk/src/ism/metadata/ccipread.ts
Refactors metadata fetching to use persistent response variable. Adds HTTP fetch logic for both inlined URLs and POST requests with centralized error handling; logs warnings on fetch failures and continues to next URL, returning parsed responseJson.data on success.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant CCTPService as CCTP Service
    participant Attestation as Attestation Endpoint
    participant Prometheus

    Client->>CCTPService: getAttestation(txHash)
    CCTPService->>Attestation: fetch attestation JSON
    Attestation-->>CCTPService: CCTPData with messages[]
    
    loop Iterate Messages
        CCTPService->>CCTPService: Check message status
        alt Status is PENDING
            CCTPService->>Prometheus: Log unhandled error (if known reason)
            CCTPService->>Client: Throw descriptive error
        else Status is other
            CCTPService->>Client: Return attestation
        end
    end
Loading
sequenceDiagram
    participant Client
    participant CCIPRead as CCIP-read Metadata
    participant URLs as URL List

    Client->>CCIPRead: Fetch metadata
    
    loop For each URL
        CCIPRead->>URLs: Attempt fetch (data-inlined or POST)
        
        alt Fetch succeeds & response ok
            CCIPRead->>CCIPRead: Parse responseJson
            CCIPRead->>Client: Return ensure0x(responseJson.data)
        else Fetch fails or response not ok
            CCIPRead->>CCIPRead: Log warning
            Note over CCIPRead: Continue to next URL
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • CCTP type extensions and PENDING detection logic: New field additions (eventNonce, cctpVersion, status, delayReason) and conditional PENDING-handling path warrant careful review for type safety and error handling correctness
  • CCIP-read response flow restructuring: Persistent response variable and refactored catch-block JSON parsing changes control flow; verify retry logic doesn't mask errors or exit prematurely
  • Prometheus metric logging: Conditional logging based on delay reasons should be verified for correct condition coverage
  • TODO comment: Note indicates incomplete multi-message-per-txHash handling; clarify scope and acceptance criteria

Possibly related PRs

Suggested reviewers

  • nambrot
  • antigremlin
  • xeno097
  • ltyu

Poem

Like an ogre with layers, these changes come in depth—
CCTP now sees what's pending with each attestation step.
CCIP-read's grown wiser, trying paths 'til one succeeds,
Proper error handling's what a sturdy codebase needs. 🌿

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main changes: improving CCTP v2 offchain lookup service by piping delay reasons and fixing the opaque error in CCIP-read metadata fetching.
Description check ✅ Passed The description covers the primary objective and includes related issues and testing details, though the Backward compatibility section is incomplete.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch cctp-v2-ccip-server

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
typescript/ccip-server/src/services/CCTPAttestationService.ts (3)

16-20: Consider making eventNonce optional for consistency.

Since eventNonce is a CCTP v2 field like the others, it should probably be optional too. This would better align with the backward compatibility pattern you've got going here.

Apply this diff:

 interface CCTPMessageEntry {
   attestation: string;
   message: string;
-  eventNonce: string;
+  eventNonce?: string;
   // CCTP v2 only
   cctpVersion?: string;
   status?: Status;
   delayReason?: DelayReason;
 }

166-178: Consider adjusting log level for expected pending states.

Looking at this, you're logging all PENDING states as errors, even the ones that are just... well, pending normally. Things like pending_confirmations are expected and temporary - they'll sort themselves out. The serious stuff (like insufficient_fee) deserves the error level, but the rest? Maybe info or warn would fit better.

Based on the past review discussion, I know you want to keep things simple and tune alerts separately, but having the right log level from the start makes life easier down the road.

Here's one way to handle it:

 json.messages.forEach((message) => {
   if (message.attestation === 'PENDING') {
     const errorString = message.delayReason 
       ? `CCTP attestation is pending due to ${message.delayReason}`
       : 'CCTP attestation is still pending';
     switch (message.delayReason) {
       case 'insufficient_fee':
       case 'amount_above_max':
       case 'insufficient_allowance_available':
         PrometheusMetrics.logUnhandledError(this.serviceName);
+        logger.error(context, errorString);
         break;
+      default:
+        logger.info(context, errorString);
+        break;
     }
-    logger.error(context, errorString);
     throw new Error(errorString);
   }
 });

166-181: The forEach loop checks all messages but only the first is returned.

You've got this loop checking every message for PENDING states, but then line 181 just grabs the first one and calls it a day. If there's only ever one message per transaction (which the TODO suggests), might be cleaner to just work with json.messages[0] directly. Otherwise, if there could be multiple messages, you'd want to track which one you're actually returning.

If single message is expected:

- json.messages.forEach((message) => {
-   if (message.attestation === 'PENDING') {
+ const message = json.messages[0];
+ if (message.attestation === 'PENDING') {
      // ... existing error handling
-   }
- });
+ }

  // TODO: handle multiple messages in one tx hash
- return [json.messages[0].message, json.messages[0].attestation];
+ return [message.message, message.attestation];
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 20c24dc and 92872d1.

📒 Files selected for processing (2)
  • typescript/ccip-server/src/services/CCTPAttestationService.ts (2 hunks)
  • typescript/sdk/src/ism/metadata/ccipread.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
typescript/sdk/src/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Keep TypeScript SDK source under typescript/sdk/src/ using the documented abstractions (MultiProvider, HyperlaneCore, MultiProtocolCore)

Files:

  • typescript/sdk/src/ism/metadata/ccipread.ts
🧬 Code graph analysis (1)
typescript/ccip-server/src/services/CCTPAttestationService.ts (1)
typescript/ccip-server/src/utils/prometheus.ts (1)
  • PrometheusMetrics (22-29)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (53)
  • GitHub Check: cli-evm-e2e-matrix (core-read)
  • GitHub Check: cli-evm-e2e-matrix (warp-rebalancer)
  • GitHub Check: cli-evm-e2e-matrix (warp-check-4)
  • GitHub Check: cli-evm-e2e-matrix (warp-extend-recovery)
  • GitHub Check: cli-evm-e2e-matrix (warp-read)
  • GitHub Check: cli-evm-e2e-matrix (warp-send)
  • GitHub Check: cli-evm-e2e-matrix (warp-extend-basic)
  • GitHub Check: cli-evm-e2e-matrix (warp-extend-config)
  • GitHub Check: cli-evm-e2e-matrix (warp-init)
  • GitHub Check: cli-evm-e2e-matrix (core-check)
  • GitHub Check: cli-evm-e2e-matrix (relay)
  • GitHub Check: cli-evm-e2e-matrix (warp-bridge-1)
  • GitHub Check: cli-evm-e2e-matrix (warp-check-5)
  • GitHub Check: cli-evm-e2e-matrix (warp-check-3)
  • GitHub Check: cli-evm-e2e-matrix (warp-deploy-2)
  • GitHub Check: cli-evm-e2e-matrix (warp-deploy-1)
  • GitHub Check: cli-evm-e2e-matrix (warp-apply-2)
  • GitHub Check: cli-evm-e2e-matrix (warp-check-2)
  • GitHub Check: cli-evm-e2e-matrix (warp-bridge-2)
  • GitHub Check: cli-evm-e2e-matrix (warp-apply-submitters)
  • GitHub Check: cli-evm-e2e-matrix (warp-check-1)
  • GitHub Check: cli-evm-e2e-matrix (warp-apply-1)
  • GitHub Check: cli-evm-e2e-matrix (core-apply)
  • GitHub Check: cli-evm-e2e-matrix (core-deploy)
  • GitHub Check: cli-evm-e2e-matrix (warp-apply-ism-updates)
  • GitHub Check: cli-evm-e2e-matrix (core-init)
  • GitHub Check: env-test-matrix (mainnet3, optimism, igp)
  • GitHub Check: env-test-matrix (mainnet3, optimism, core)
  • GitHub Check: env-test-matrix (mainnet3, inevm, igp)
  • GitHub Check: env-test-matrix (mainnet3, ethereum, igp)
  • GitHub Check: env-test-matrix (mainnet3, inevm, core)
  • GitHub Check: env-test-matrix (mainnet3, arbitrum, igp)
  • GitHub Check: env-test-matrix (testnet4, sepolia, core)
  • GitHub Check: cli-cosmos-e2e-matrix (warp-read)
  • GitHub Check: env-test-matrix (mainnet3, ethereum, core)
  • GitHub Check: env-test-matrix (mainnet3, arbitrum, core)
  • GitHub Check: cli-cosmos-e2e-matrix (core-check)
  • GitHub Check: cli-cosmos-e2e-matrix (warp-deploy)
  • GitHub Check: cli-cosmos-e2e-matrix (core-read)
  • GitHub Check: cli-cosmos-e2e-matrix (core-apply)
  • GitHub Check: cli-cosmos-e2e-matrix (core-deploy)
  • GitHub Check: cosmos-sdk-e2e-run
  • GitHub Check: cli-cross-chain-e2e-matrix (warp-apply)
  • GitHub Check: cli-cross-chain-e2e-matrix (warp-deploy)
  • GitHub Check: cli-radix-e2e-matrix (core-deploy)
  • GitHub Check: yarn-test-run
  • GitHub Check: cli-install-test-run
  • GitHub Check: build-and-push-to-gcr
  • GitHub Check: lint-rs
  • GitHub Check: lander-coverage
  • GitHub Check: test-rs
  • GitHub Check: e2e-matrix (evm)
  • GitHub Check: agent-configs (testnet4)

Comment on lines +166 to +178
json.messages.forEach((message) => {
if (message.attestation === 'PENDING') {
const errorString = `CCTP attestation is pending due to ${message.delayReason}`;
switch (message.delayReason) {
case 'insufficient_fee':
case 'amount_above_max':
case 'insufficient_allowance_available':
PrometheusMetrics.logUnhandledError(this.serviceName);
}
logger.error(context, errorString);
throw new Error(errorString);
}
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Handle undefined delayReason to avoid confusing error messages.

Right now, if delayReason is undefined (which it can be for normal pending states), you'll get an error message like "CCTP attestation is pending due to undefined". Not exactly helpful when you're trying to figure out what went wrong.

Consider this approach:

 json.messages.forEach((message) => {
   if (message.attestation === 'PENDING') {
-    const errorString = `CCTP attestation is pending due to ${message.delayReason}`;
+    const errorString = message.delayReason 
+      ? `CCTP attestation is pending due to ${message.delayReason}`
+      : 'CCTP attestation is still pending';
     switch (message.delayReason) {
       case 'insufficient_fee':
       case 'amount_above_max':
       case 'insufficient_allowance_available':
         PrometheusMetrics.logUnhandledError(this.serviceName);
+        break;
     }
     logger.error(context, errorString);
     throw new Error(errorString);
   }
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
json.messages.forEach((message) => {
if (message.attestation === 'PENDING') {
const errorString = `CCTP attestation is pending due to ${message.delayReason}`;
switch (message.delayReason) {
case 'insufficient_fee':
case 'amount_above_max':
case 'insufficient_allowance_available':
PrometheusMetrics.logUnhandledError(this.serviceName);
}
logger.error(context, errorString);
throw new Error(errorString);
}
});
json.messages.forEach((message) => {
if (message.attestation === 'PENDING') {
const errorString = message.delayReason
? `CCTP attestation is pending due to ${message.delayReason}`
: 'CCTP attestation is still pending';
switch (message.delayReason) {
case 'insufficient_fee':
case 'amount_above_max':
case 'insufficient_allowance_available':
PrometheusMetrics.logUnhandledError(this.serviceName);
break;
}
logger.error(context, errorString);
throw new Error(errorString);
}
});
🤖 Prompt for AI Agents
In typescript/ccip-server/src/services/CCTPAttestationService.ts around lines
166-178, the error string uses message.delayReason directly which can be
undefined and produce "due to undefined"; update the code to normalize the
delayReason (e.g. const reason = message.delayReason ?? 'unknown' or 'not
provided') and use that normalized value in the errorString and log, keep the
existing PrometheusMetrics logging for the specific known delay reasons, and
ensure logs/errors remain descriptive by including the fallback reason when
delayReason is missing.

Comment on lines +90 to 99
const responseJson = await res.json();
if (!res.ok) {
this.core.logger.warn(
`Server at ${url} responded with error: ${responseJson.error}`,
);
// try next URL
continue;
} else {
return ensure0x(responseJson.data);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Mind the mud when parsing responses.

If a server answers with plain text or malformed JSON, res.json() will throw before we can slog on to the next URL, recreating the opaque failure we’re trying to fix. Please catch that parse error and keep looping so we always have a fallback path.

-      const responseJson = await res.json();
-      if (!res.ok) {
-        this.core.logger.warn(
-          `Server at ${url} responded with error: ${responseJson.error}`,
-        );
-        // try next URL
-        continue;
-      } else {
-        return ensure0x(responseJson.data);
-      }
+      let responseJson: any;
+      try {
+        responseJson = await res.json();
+      } catch (error) {
+        this.core.logger.warn(
+          `Server at ${url} returned non-JSON payload: ${error}`,
+        );
+        continue;
+      }
+      if (!res.ok) {
+        this.core.logger.warn(
+          `Server at ${url} responded with error: ${responseJson?.error}`,
+        );
+        continue;
+      }
+      return ensure0x(responseJson.data);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const responseJson = await res.json();
if (!res.ok) {
this.core.logger.warn(
`Server at ${url} responded with error: ${responseJson.error}`,
);
// try next URL
continue;
} else {
return ensure0x(responseJson.data);
}
let responseJson: any;
try {
responseJson = await res.json();
} catch (error) {
this.core.logger.warn(
`Server at ${url} returned non-JSON payload: ${error}`,
);
continue;
}
if (!res.ok) {
this.core.logger.warn(
`Server at ${url} responded with error: ${responseJson?.error}`,
);
continue;
}
return ensure0x(responseJson.data);
🤖 Prompt for AI Agents
In typescript/sdk/src/ism/metadata/ccipread.ts around lines 90 to 99, calling
await res.json() can throw on plain-text or malformed responses which breaks the
retry loop; wrap the JSON parse in a try/catch, on parse failure read res.text()
(or use a safe parse) and log a warning containing the URL and raw text/error,
then continue to the next URL instead of letting the exception bubble up; keep
the existing behavior of returning ensure0x(responseJson.data) only when parsing
succeeds and res.ok is true.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

3 participants